package io.quarkus.awt.it;

import static io.quarkus.awt.it.TestUtil.checkLog;
import static io.quarkus.awt.it.TestUtil.compareArrays;
import static io.quarkus.awt.it.TestUtil.decodeArray4;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import io.quarkus.test.junit.QuarkusTest;

/**
 * Tests image decoders, both raster data and image metadata.
 *
 * The source for many test images is:
 * https://en.wikipedia.org/wiki/Grace_Hopper#/media/File:Commodore_Grace_M._Hopper,_USN_(covered).jpg
 * File: Commodore Grace M. Hopper, USN (covered)
 * Created: 19 January 1984
 * The rest of the imagery is an original work of the committer.
 */
@QuarkusTest
public class ImageDecodersTest {

    /**
     * When comparing pixel colour values, how much difference
     * from the expected value is allowed.
     * 0 means no difference is tolerated.
     */
    private static final int[] PIXEL_DIFFERENCE_THRESHOLD_RGBA_VEC = new int[] { 0, 0, 0, 0 };

    /**
     * Exercises decoders on ordinary, valid images.
     * Mostly GIMP or ImageMagic generated.
     *
     * We want to make sure native-image compiled Java libs
     * can read files generated by different programs,
     * not just images also generated with Java...
     *
     * @param testData valid image filename █ RGBA pixel to test
     */
    @ParameterizedTest
    // @formatter:off
    @ValueSource(strings = {
    // File name       █Sanity check pixel x 100 y 100
    "test_anim.gif     █13,0,0,0",
    "test.bmp          █14,3,23,0",
    "test_bmp3.bmp     █14,3,23,0",
    "test_comp.jp2     █14,3,23,0",
    "test_deflate.tiff █182,104,180,255",
    "test.gif          █4,0,0,0",
    "test_gray.jpg     █12,0,0,0",
    "test_hsl.jpg      █21,21,23,0",
    "test_hsl.png      █14,3,23,0",
    "test.jp2          █14,3,23,0",
    "test.jpg          █19,0,24,0",
    "test_lossless.jpg █15,3,23,0",
    "test_lzw.tiff     █14,3,23,0",
    "test_none.tiff    █182,104,180,255",
    "test_packbits.tiff█182,104,180,255",
    "test.png          █14,3,23,0",
    "test_rgb.png      █1,0,2,0",
    "test_srgb.png     █14,3,23,0",
    "test.tiff         █14,3,23,0",
    "test.wbmp         █0,0,0,0",
    "test_zip.png      █14,3,23,0",
    })
    // @formatter:on
    public void testDecoders(String testData) throws IOException {
        final String[] filePixel = testData.split("█");
        final String fileName = filePixel[0].trim();
        final byte[] imgBytes = given()
                .multiPart("image", new File(ImageDecodersTest.class.getResource("/ordinary/" + fileName).getFile()))
                .when()
                .post("/topng/" + fileName)
                .asByteArray();
        final BufferedImage image = ImageIO.read(new ByteArrayInputStream(imgBytes));

        assertNotNull(image, fileName + ": The image returned is not a valid PNG.");

        assertTrue(image.getWidth() == 237 && image.getHeight() == 296,
                String.format("%s image's expected dimension is %d x %d, but was %d x %d.",
                        fileName, 237, 296, image.getWidth(), image.getHeight()));

        final int[] expected = decodeArray4(filePixel[1].trim());
        final int[] actual = new int[4]; //4BYTE RGBA
        image.getData().getPixel(100, 100, actual);
        assertTrue(compareArrays(expected, actual, PIXEL_DIFFERENCE_THRESHOLD_RGBA_VEC),
                String.format("%s: Wrong pixel. Expected: [%d,%d,%d,%d] Actual: [%d,%d,%d,%d]", fileName,
                        expected[0], expected[1], expected[2], expected[3],
                        actual[0], actual[1], actual[2], actual[3]));
    }

    // @formatter:off
    /**
     * Suite of either specifically invalid images, to trigger exceptions
     * during parsing, or more exotic images, utilizing e.g. colour profiles
     * or specific metadata.
     *
     * Notes on selected images:
     *
     *   weird_230.bmp: Triggers an error that is supposed to have a text
     *   in src/java.desktop/share/classes/com/sun/imageio/plugins/common/iio-plugin.properties
     *   associated with it. If the native image works right, the expected text should be captured
     *   in the log.
     *
     * See the test data spreadsheet below for more:
     *
     * OK  - must produce a readable BufferedImage, metadata could be checked with a regexp
     * NOK - must not produce any readable image, i.e. must fail, Quarkus log is checked with a regexp
     * NA  - undefined, e.g. JDK inconsistent across versions, we just want no JNI/static init errors,
     *       e.g. https://mail.openjdk.java.net/pipermail/client-libs-dev/2022-May/004780.html
     */
    @ParameterizedTest
    @ValueSource(strings = {
    // File name            █OK/NOK/NA█ Optionally a regexp to look for in the log or image toString()
    "weird_1000.jpg         █NOK█.*weird_1000.jpg.*Bogus Huffman table definition.*" , // Affects javax/imageio/IIOException
    "test_a98.jpg           █OK █.*type = 5 .* #pixelBits = 24 .*", // Affects sun.java2d.cmm.lcms.LCMSTransform
    "test_gray_to_k.tiff    █OK █.*type = 5 .* #pixelBits = 24 .*", // Affects java.awt.color.ICC_Profile
    "test_gray_to_k.jpg     █NOK█.*test_gray_to_k.jpg.*(Can not access.*profile|LUT is not suitable).*", // Affects java.awt.color.ICC_Profile
    "test_ps_gray.tiff      █OK █.*type = 5 .* #pixelBits = 24 .*", // Affects java.awt.color.ICC_ProfileGray
    "test_sRGB_ICC_Miss.tiff█OK █.*type = 5 .* #pixelBits = 24 .*", // Affects java.awt.color.CMMException
    "weird_488-1.tif        █OK █.*type = 11 .* #pixelBits = 16 .*", // Affects sun.awt.image.*Raster
    "test_hyperstack.tiff   █OK █.*type = 5 .* #pixelBits = 24 numComponents = 3 .*", // Affects tiff plugin
    "test_lut_3c_fiji.tiff  █OK █.*type = 5 .* #pixelBits = 24 numComponents = 3 .*", // Life sciences imagery, Fiji used to edit colour Lookup Table (LUT)
    "test_jpeg.tiff         █NA █(?i:.*(type = 6 .* #pixelBits = 32 numComponents = 4 |test_jpeg.tiff.*Unsupported Image Type).*)", // JPEG compression TIFF by GIMP, bogus, inconsistent in JDK
    "test_jpeg_2.tiff       █NOK█.*test_jpeg_2.tiff.*Unsupported Image Type.*", // JPEG compression TIFF by GIMP, Transparent pixels color saved
    "weird_230.bmp          █NOK█.*weird_230.bmp.*New BMP version not implemented yet.*" // Tested with a custom iio-plugin.properties too
    })
    // @formatter:on
    public void testComplexImages(String testData) throws IOException {
        final String[] imgExpectations = testData.split("█");
        final String fileName = imgExpectations[0].trim();
        final String okNoKNA = imgExpectations[1].trim();
        final Pattern pattern = (imgExpectations.length > 2) ? Pattern.compile(imgExpectations[2].trim()) : null;
        final byte[] imgBytes = given()
                .multiPart("image", new File(ImageDecodersTest.class.getResource("/complex/" + fileName).getFile()))
                .when()
                .post("/topng/" + fileName)
                .asByteArray();
        final BufferedImage image = ImageIO.read(new ByteArrayInputStream(imgBytes));
        switch (okNoKNA) {
            case "OK":
                assertNotNull(image, "The image " + fileName + " should have been converted to a valid PNG.");
                if (pattern != null) {
                    assertTrue(pattern.matcher(image.toString()).matches(),
                            "Image description should have matched " + pattern);
                }
                break;
            case "NOK":
                assertNull(image, "The image " + fileName + " should have triggered a parsing error.");
                if (pattern != null) {
                    checkLog(pattern, fileName);
                }
                break;
            case "NA":
                if (pattern != null) {
                    if (image == null) {
                        checkLog(pattern, fileName);
                    } else {
                        assertTrue(pattern.matcher(image.toString()).matches(),
                                "Image description should have matched " + pattern);
                    }
                }
                break;
            default:
                fail("Bogus test data: unknown condition: " + okNoKNA);
                break;
        }
    }
}
